﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using ZackFlame.IronBrainFuck.Parser;

namespace ZackFlame.IronBrainFuck.JIT
{
    /// <summary>
    /// Представляет компилятор BrainFuck программы.
    /// </summary>
    public sealed class BrainCompiler
    {
        static readonly MethodInfo writeChar, writeCell, readChar, readCell;

        static BrainCompiler()
        {
            writeChar = typeof(IOHelper).GetMethod(
                "WriteChar",
                BindingFlags.Static | BindingFlags.Public);

            writeCell = typeof(IOHelper).GetMethod(
                "WriteCell",
                BindingFlags.Static | BindingFlags.Public);

            readChar = typeof(IOHelper).GetMethod(
                "ReadChar",
                BindingFlags.Static | BindingFlags.Public);

            readCell = typeof(IOHelper).GetMethod(
                "ReadCell",
                BindingFlags.Static | BindingFlags.Public);
        }

        /// <summary>
        /// Генерирует IL для заданной BrainFuck программы.
        /// </summary>
        /// <param name="il">
        /// Генератор IL кода. Обычно берётся из DynamicMethod.GetILGenerator()
        /// или MethodBuilder.GetILGenerator().
        /// </param>
        /// <param name="brainFuckProgram">BrainFuck программа.</param>
        public static void GenerateIL(ILGenerator il, string brainFuckProgram)
        {
            // Стек циклов [[[...]]]
            var labels = new Stack<KeyValuePair<Label, Label>>();

            // Объявляем локальную переменную с CellIndex
            il.DeclareLocal(typeof(UInt16));
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Stloc_0);

            BrainAction[] program = BrainParser.Parse(brainFuckProgram);

            for (int i = 0; i < program.Length; i++)
            {
                switch (program[i].Operation)
                {
                    case BrainOperation.Add:
                        EmitHelper.EmitAdd(il, program[i].Count);
                        break;

                    case BrainOperation.Sub:
                        EmitHelper.EmitSub(il, program[i].Count);
                        break;

                    case BrainOperation.Right:
                        EmitHelper.EmitRight(il, program[i].Count);
                        break;

                    case BrainOperation.Left:
                        EmitHelper.EmitLeft(il, program[i].Count);
                        break;

                    case BrainOperation.CycleOpen:
                        Label label1 = il.DefineLabel();
                        Label label2 = il.DefineLabel();
                        labels.Push(new KeyValuePair<Label, Label>(label1, label2));
                        il.MarkLabel(label1);
                        il.Emit(OpCodes.Ldarg_0);
                        il.Emit(OpCodes.Ldloc_0);
                        il.Emit(OpCodes.Ldelem_U2);
                        il.Emit(OpCodes.Brfalse, label2);
                        break;

                    case BrainOperation.CycleClose:
                        if (labels.Count == 0)
                            throw new InvalidProgramException("Opening bracket expected");

                        var labelPair = labels.Pop();
                        il.Emit(OpCodes.Br, labelPair.Key);
                        il.MarkLabel(labelPair.Value);
                        break;

                    case BrainOperation.Write:
                        il.Emit(OpCodes.Ldarg_0);
                        il.Emit(OpCodes.Ldloc_0);
                        il.Emit(OpCodes.Ldelem_U2);
                        il.Emit(OpCodes.Ldarg_3);
                        il.EmitCall(OpCodes.Call, writeChar, null);
                        break;

                    case BrainOperation.Read:
                        il.Emit(OpCodes.Ldarg_0);
                        il.Emit(OpCodes.Ldloc_0);
                        il.Emit(OpCodes.Ldarg_2);
                        il.EmitCall(OpCodes.Call, readChar, null);
                        il.Emit(OpCodes.Stelem_I2);
                        break;

                    case BrainOperation.SpecialWrite:
                        il.Emit(OpCodes.Ldloc_0);
                        il.Emit(OpCodes.Ldarg_0);
                        il.Emit(OpCodes.Ldloc_0);
                        il.Emit(OpCodes.Ldelem_U2);
                        il.Emit(OpCodes.Ldarg_3);
                        il.EmitCall(OpCodes.Call, writeCell, null);
                        break;

                    case BrainOperation.SpecialRead:
                        il.Emit(OpCodes.Ldarg_0);
                        il.Emit(OpCodes.Ldloc_0);
                        il.Emit(OpCodes.Ldarg_2);
                        il.EmitCall(OpCodes.Call, readCell, null);
                        il.Emit(OpCodes.Stelem_I2);
                        break;

                    case BrainOperation.Zero:
                        il.Emit(OpCodes.Ldarg_0);
                        il.Emit(OpCodes.Ldloc_0);
                        il.Emit(OpCodes.Ldc_I4_0);
                        il.Emit(OpCodes.Stelem_I2);
                        break;
                }
            }

            // Возвращаем CellIndex
            il.Emit(OpCodes.Ldloc_0);
            il.Emit(OpCodes.Ret);

            if (labels.Count > 0)
                throw new InvalidProgramException("Closing bracket expected");
        }

        /// <summary>
        /// Компилирует BrainFuck программу в IL (только для исполнения в памяти,
        /// для компиляции в сборку используйте BrainFuckCompiler).
        /// </summary>
        /// <param name="brainFuckProgram">BrainFuck программа.</param>
        /// <returns>Скомпилированная в IL программа.</returns>
        public BrainMethod Compile(string brainFuckProgram)
        {
            DynamicMethod method = new DynamicMethod(
                "Execute",
                typeof(UInt16),
                new Type[] 
                {
                    typeof(UInt16[]), typeof(UInt16), // Cells, CellIndex
                    typeof(TextReader), typeof(TextWriter) // Input, Output
                });

            ILGenerator il = method.GetILGenerator();
            GenerateIL(il, brainFuckProgram);
            
            // Возвращаем готовый метод
            return new BrainMethod(brainFuckProgram, method);
        }
    }
}
